package com.google.code;

import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author Leopoldo Muller
 */
public class BunitContext {

    private Class<?> testClass;
    private AbstractApplicationContext context;

    /**
     * Creates the test context. Use {@code testClass} {@link Context} annotation for optional additional XML class-path
     * locations. Register the bean post processor to inject {@link Given} fields. Scan the {@code testClass} package
     * for additional beans. Register {@link Create} method of all beans. By the end register the test bean.
     * 
     * @param testClass
     *            scenario to be user as reference
     */
    public BunitContext(Class<?> testClass) {

        this.testClass = testClass;

        List<String> locations = new ArrayList<String>();
        locations.add("bunit-context.xml");

        if (testClass.isAnnotationPresent(Context.class)) {

            for (String location : testClass.getAnnotation(Context.class).value()) {

                locations.add(location);
            }
        }

        context = new ClassPathXmlApplicationContext(locations.toArray(new String[] {}));

        scanTestClassPackage();
        registerBeansCreateMethods();
        registerTestBeanDefinition();
    }

    /**
     * Register {@link Create} methods of all context beans.
     */
    void registerBeansCreateMethods() {

        for (String name : context.getBeanDefinitionNames()) {

            registerCreateMethods(name);
        }
    }

    /**
     * Shortcut to {@link BeanDefinitionRegistry} type cast.
     * 
     * @return the bean factory
     */
    BeanDefinitionRegistry getRegistry() {

        return ((BeanDefinitionRegistry) context.getBeanFactory());
    }

    /**
     * Register the instance of {@code testClass} in {@code context}.
     */
    void registerTestBeanDefinition() {

        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(testClass);
        AbstractBeanDefinition definition = builder.getBeanDefinition();

        getRegistry().registerBeanDefinition("Bunit.testClass", definition);
    }

    /**
     * Scan test case class path for additional beans.
     */
    void scanTestClassPackage() {

        new ClassPathBeanDefinitionScanner(getRegistry()).scan(testClass.getPackage().getName());
    }

    /**
     * Returns the test instance with {@link Given} fields set.
     * 
     * @return the test case instance.
     */
    public Object getTestInstance() {

        return context.getBean("Bunit.testClass");
    }

    /**
     * Shortcut to {@code context.getBean()}.
     * 
     * @param name
     *            bean name
     * 
     * @return
     * 
     *         the instance of bean or {@code null} if the {@code context} doesn't contains the bean.
     */
    Object get(String name) {

        Object bean = null;

        if (context.containsBean(name)) {

            bean = context.getBean(name);
        }

        return bean;
    }

    /**
     * Register {@link Create} methods of a bean.
     * 
     * @param beanName
     *            bean name to register {@link Create} methods
     */
    void registerCreateMethods(String beanName) {

        for (Method m : context.getType(beanName).getMethods()) {

            if (m.isAnnotationPresent(Create.class)) {

                registerCreateMethod(m, beanName);
            }
        }
    }

    /**
     * Use bean creation via instance factory method for a {@link Create} method. Use {@link Create#value()} as alias to
     * registered bean.
     * 
     * @param method
     *            the {@link Create} method
     * 
     * @param beanName
     *            the bean to run {@link Create} method as factory method
     */
    void registerCreateMethod(Method method, String beanName) {

        String name = beanName + '#' + method.getName();
        String alias = method.getAnnotation(Create.class).value();

        AbstractBeanDefinition definition = genericBeanDefinition(method.getReturnType()).getBeanDefinition();
        definition.setFactoryBeanName(beanName);
        definition.setFactoryMethodName(method.getName());

        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context.getBeanFactory();
        registry.registerBeanDefinition(name, definition);
        registry.registerAlias(name, alias);
    }
}
